/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.tracking.injection;

import static ch.trackedbean.tracking.internal.injection.InjectionUtils.*;
import java.io.*;
import sun.misc.*;
import ch.trackedbean.tracking.internal.*;
import ch.trackedbean.tracking.internal.injection.*;

/**
 * Class loader which implements the calls {@link TrackedBeanAdapter#adapt(IClassInformation,byte[])} for each class implementing a specified marker interface.<br>
 * Child classes of the marked classes were also adapted so that they use the features of the {@link StatusInformationInternal} interface.
 * 
 * @author M. Hautle
 */
public class TrackedBeanInjection extends ClassLoader {
    /** The name of the default marker interface. */
    private static final String DEFAULT_MARKER = "ch.trackedbean.tracking.TrackedBean";

    /** Object representing the bootstrap classpath. */
    private final URLClassPath BOOTSTRAP_CP = sun.misc.Launcher.getBootstrapClassPath();

    /** The class analyzer to use. */
    private final AbstractClassAnalyzer analyzer;

    /** The class file adapter. */
    private final TrackedBeanAdapter adapter;

    /**
     * Default constructor.<br>
     * Sets this classloader as thread context classloader
     * 
     * @param markerInterface The fully qualified name of the marker interface for the classes to adapt
     */
    public TrackedBeanInjection(String markerInterface) {
        Thread.currentThread().setContextClassLoader(this);
        analyzer = new AbstractClassAnalyzer(markerInterface) {
            @Override
            protected InputStream getResourceStream(String name) throws Exception {
                return getResourceAsStream(name);
            }
        };
        adapter = new TrackedBeanAdapter(analyzer);
        // LATER own bootstrap checker?
        // String[] libs = System.getProperty("sun.boot.class.path").split(File.pathSeparator);
    }

    /**
     * Launches an the application by calling it's main method.<br>
     * Use this method to "activate" the Bean Tracking in an application.
     * 
     * @param mainClass The name of the class holding the main method
     * @param args The arguments to pass to the main
     * @throws Exception If something went wrong
     */
    public static void launchApplication(String mainClass, String[] args) throws Exception {
        launchApplication(DEFAULT_MARKER, mainClass, args);
    }

    /**
     * Launches an the application by calling it's main method.<br>
     * Use this method to "activate" the Bean Tracking in an application.
     * 
     * @param markerInterface The marker interface for the classes to adapt
     * @param mainClass The name of the class holding the main method
     * @param args The arguments to pass to the main
     * @throws Exception If something went wrong
     */
    public static void launchApplication(String markerInterface, String mainClass, String[] args) throws Exception {
        new TrackedBeanInjection(markerInterface).loadClass(mainClass).getMethod("main", new Class[] { String[].class }).invoke(null, new Object[] { args });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
        // First, check if the class has already been loaded
        Class c = findLoadedClass(name);
        if (c == null) {
            // load java classes via original class loader
            if (name.startsWith("java."))
                return super.loadClass(name, resolve);
            try {
                final String internalName = name.replace('.', '/');
                // gets an input stream to read the bytecode of the class
                final String resource = internalName.concat(".class");
                // delegate to the parent if it is a bootstrap class
                if (BOOTSTRAP_CP.getResource(resource) != null)
                    return super.loadClass(name, resolve);
                definePackage(name);
                // read the class and adapt it if necessary
                final byte[] b = adapt(readClass(getResourceAsStream(resource)), internalName);
                c = defineClass(name, b, 0, b.length);
            } catch (final Exception e) {
                // e.printStackTrace();
                throw new ClassNotFoundException(name, e);
            }
        }
        if (resolve)
            resolveClass(c);
        return c;
    }

    /**
     * Adapts the passed class byte code if necessary.
     * 
     * @param b A byte array holding the class file
     * @param name The internal class name
     * @return The adapted bytecode
     * @throws IOException If something went wrong
     */
    private byte[] adapt(byte[] b, String name) throws IOException {
        final ClassInformation ci = analyzer.getInformation(name, b);
        if (ci.tracked || ci.parentTracked)
            return adapter.adapt(ci, b);
        return b;
    }

    /**
     * Defines the package for the given class name.<br>
     * If the package already exists this method simply returns, otherwise the package gets created.
     * 
     * @param name The name of the class
     */
    private void definePackage(final String name) {
        int p = name.lastIndexOf('.');
        if (p < 0)
            return;
        final String pack = name.substring(0, p);
        if (getPackage(pack) != null)
            return;
        definePackage(pack, null, null, null, null, null, null, null);
    }
}
